﻿/* SleepyLabs is a simple clean shutdown/restart application for XenServer
 * Copyright (C) 2010 Stephen Rice
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 * Web: http://code.google.com/p/sleepylabs/
 * Date: 15/06/2010
 * 
 */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using XenAPI;
using SleepyLabs.Properties;
using System.IO;
using System.Threading;

namespace SleepyLabs
{
    /// <summary>
    /// The main form for SleepyLabs. Displays the UI and coordinates the user 
    /// interaction, cache and task manager.
    /// </summary>
    public partial class SleepyLabsForm : Form
    {
        /// <summary>
        /// On startup XenServer searches for any VM with this key set in it's 
        /// other config map. If present and set to true XenServer attempts to
        /// perform a non intellegent (i.e without placement considerations)
        /// start of the VM.
        /// </summary>
        private const string AUTO_RESTART_KEY = "auto_poweron";
        private const string AUTO_RESTART_VALUE = "true";
        private const string SAVE_PATH = "SleepyLabs/";
        private const string SAVE_FILE = "data";

        private Session session;

        /// <summary>
        /// Simple, dumb cache used for updating the UI. Does not use event
        /// handlers and requires manual refreshing.
        /// </summary>
        public List<VM> VMCache = new List<VM>();

        /// <summary>
        /// Coordinates functions which take enough time that they cannot
        /// run on the UI thread. These are generally server calls.
        /// </summary>
        private TaskManager taskManager;

        private VM SelectedVM
        {
            get
            {
                if (dataGridViewVMs.SelectedRows.Count != 1)
                    return null;

                return dataGridViewVMs.SelectedRows[0].Tag as VM;
            }
        }

        public SleepyLabsForm()
        {
            InitializeComponent();
            ReadServerURL();
            taskManager = new TaskManager(toolStripProgressBar1, toolStripStatusLabel1);
            taskManager.TasksStarted += new EventHandler<EventArgs>(disableAll);
            taskManager.AllTasksFinished += new EventHandler<EventArgs>(enableAll);
            updateEnabledState();
        }

        private void doBackGroundWork(DoWorkEventHandler method, object args, string title)
        {
            taskManager.AddTask(method, args, title);
        }

        /// <summary>
        /// Updates the visibility and enablement of all the controls
        /// depending on our state.
        /// </summary>
        private void updateEnabledState()
        {
            labelDisconnected.Visible = session == null;
            buttonShutDown.Enabled = session != null;
            buttonConnect.Text = session == null ? Messages.CONNECT : Messages.DISCONNECT;
            textBoxServerIP.Enabled = session == null;
            textBoxPassword.Enabled = session == null;
            buttonRefresh.Enabled = session != null;
            if (dataGridViewVMs.SelectedRows.Count != 1)
            {
                buttonDisable.Enabled = false;
                buttonEnable.Enabled = false;
                return;
            }
            buttonDisable.Enabled = IsAutoRestartVM(SelectedVM);
            buttonEnable.Enabled = !buttonDisable.Enabled;
        }

        /// <summary>
        /// Task started event handler target which disables all the UI
        /// controls to minimize action interference.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="args">Not used</param>
        private void disableAll(object sender, EventArgs args)
        {
            buttonConnect.Enabled = false;
            buttonShutDown.Enabled = false;
            textBoxServerIP.Enabled = false;
            textBoxPassword.Enabled = false;
            buttonRefresh.Enabled = false;
            buttonDisable.Enabled = false;
            buttonEnable.Enabled = false;
            dataGridViewVMs.Enabled = false;
        }

        /// <summary>
        /// Tasks finished event handler which re enables UI controls
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="args">Not used</param>
        private void enableAll(object sender, EventArgs args)
        {
            buttonConnect.Enabled = true;
            dataGridViewVMs.Enabled = true;
            updateEnabledState();
        }

        /// <summary>
        /// Connects to the server specified by the ip address/credentials
        /// text boxes.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Not used</param>
        private void connectToServer(object sender, DoWorkEventArgs e)
        {
            string ip = textBoxServerIP.Text;
            Session s = new Session(ip);
            s.login_with_password("root", textBoxPassword.Text);
            saveServerURL(ip);
            // Invoke back onto the UI thread to update the session reference
            // and refresh the UI
            Program.DInvoke(new MethodInvoker(delegate()
            {
                session = s;
                refreshAll();
            }));
        }

        /// <summary>
        /// Saves the specified URL to a data file in the current users app
        /// data directory.
        /// </summary>
        /// <param name="url">The URL to save </param>
        private void saveServerURL(string url)
        {
            string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            path = string.Format("{0}/{1}", path, SAVE_PATH);
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            path += SAVE_FILE;
            TextWriter writer = null;
            try
            {
                writer = new StreamWriter(path, false);
                writer.WriteLine(url);
            }
            finally
            {
                if (writer != null)
                    writer.Close();
            }
        }

        /// <summary>
        /// Retrieves a saved URL from the users app data directory (if such
        /// a URL exists) and enters it into the server IP text box.
        /// </summary>
        private void ReadServerURL()
        {
            string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            path = string.Format("{0}/{1}{2}", path, SAVE_PATH, SAVE_FILE);
            if (!File.Exists(path))
                return;

            TextReader reader = null;
            try
            {
                reader = new StreamReader(path, false);
                textBoxServerIP.Text = reader.ReadLine();
            }
            finally
            {
                if (reader != null)
                    reader.Close();
            }
        }

        private void buttonEnable_Click(object sender, EventArgs args)
        {
            if (dataGridViewVMs.SelectedRows.Count != 1)
                return;

            doBackGroundWork(EnableAutoStart, SelectedVM, Messages.ENABLING_AUTO_RESTART);
        }

        /// <summary>
        /// Sets the auto restart flag on a VM and updates the UI.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Set e.Argument as the target VM</param>
        private void EnableAutoStart(object sender, DoWorkEventArgs e)
        {
            VM vm = e.Argument as VM;
            VM.add_to_other_config(session, vm.opaque_ref, AUTO_RESTART_KEY, AUTO_RESTART_VALUE);
            // Invoke back onto the UI thread to update the VM table
            Program.DInvoke(new MethodInvoker(delegate()
            {
                refreshAll();
            }));
        }

        private void buttonDisable_Click(object sender, EventArgs args)
        {
            if (dataGridViewVMs.SelectedRows.Count != 1)
                return;

            doBackGroundWork(DisableAutoStart, SelectedVM, Messages.DISABLING_AUTO_RESTART);
        }

        /// <summary>
        /// Removes the auto restart flag on a VM and updates the UI.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Set e.Argument as the target VM</param>
        private void DisableAutoStart(object sender, DoWorkEventArgs e)
        {
            VM vm = e.Argument as VM;
            VM.remove_from_other_config(session, vm.opaque_ref, AUTO_RESTART_KEY);
            // Invoke back onto the UI thread to update the VM table
            Program.DInvoke(new MethodInvoker(delegate()
            {
                refreshAll();
            }));
        }

        private void buttonShutDown_Click(object sender, EventArgs e)
        {
            doBackGroundWork(ShutDownAll, null, Messages.SHUTTING_DOWN_SERVERS);
        }

        /// <summary>
        /// Shuts down every server in the connected pool, starting with the 
        /// master. VMs are gently shut down first, with a hard shut down if
        /// they do not respond.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Not used</param>
        private void ShutDownAll(object sender, DoWorkEventArgs e)
        {
            // Shut down the slaves first then the master
            if (session == null)
                return;

            Pool pool = null;
            foreach (Pool p in Pool.get_all_records(session).Values)
            {
                pool = p;
                break;
            }
            if (pool.ha_enabled)
            {
                // It doesnt make sense to be shutting down VMs if HA is turned
                // on.
                Program.DInvoke(new MethodInvoker(delegate()
                {
                    MessageBox.Show(this, Messages.NOT_COMPATIBLE_WITH_HA, Messages.SHUTTING_DOWN_SERVERS);
                }));
                return;
            }
            List<Host> slaves = new List<Host>();
            Host master = null;
            foreach (Host h in Host.get_all_records(session).Values)
            {
                if (h.opaque_ref == pool.master.ServerOpaqueRef)
                    master = h;
                else
                    slaves.Add(h);
            }
            foreach (Host h in slaves)
                ShutDownHost(h);
                
            ShutDownHost(master);

            Program.DInvoke(new MethodInvoker(delegate()
                {
                    MessageBox.Show(this, Messages.SHUT_DOWN_SUCCESSFUL, Messages.SHUTTING_DOWN_SERVERS);
                    Close();
                }));
        }

        private void ShutDownHost(Host h)
        {
            bool enabledState = h.enabled;
            try
            {
                // You need to disable the host befor you can shut it down.
                Host.disable(session, h.opaque_ref);
                VM vm;
                foreach (XenRef<VM> vmRef in h.resident_VMs)
                {
                    vm = VM.get_record(session, vmRef.ServerOpaqueRef);
                    // Note that the power state check means we don't need to
                    // filter out Templates or Snapshots
                    if (vm.power_state == vm_power_state.Running && !vm.is_control_domain)
                    {
                        try
                        {
                            VM.clean_shutdown(session, vmRef.ServerOpaqueRef);
                        }
                        catch
                        {
                            VM.hard_shutdown(session, vmRef.ServerOpaqueRef);
                        }
                    }
                }
                Host.shutdown(session, h.opaque_ref);
            }
            catch (Exception e)
            {
                MessageBox.Show(this, string.Format(Messages.FAILED_TO_SHUTDOWN_W_REASON, e.Message), Messages.SHUTTING_DOWN_SERVERS);
                try
                {
                    if (!h.enabled && enabledState)
                        Host.enable(session, h.opaque_ref);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(this, string.Format(Messages.FAILED_TO_REENABLE_W_REASON, ex.Message), Messages.SHUTTING_DOWN_SERVERS);                 
                }
            }
            
        }

        private void buttonConnect_Click(object sender, EventArgs e)
        {
            if (session == null)
                doBackGroundWork(connectToServer, textBoxServerIP.Text, Messages.CONNECTING_TO_MASTER);
            else
                doBackGroundWork(disconnect, session, Messages.DISCONNECTING_FROM_MASTER);
        }

        /// <summary>
        /// Disconnects from the current server master and refreshes the UI
        /// to match the new state.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void disconnect(object sender, DoWorkEventArgs e)
        {
            Session s = e.Argument as Session;
            if (s != null)
                s.logout();
            // Invoke onto the UI thread to update
            SleepyLabsForm.ActiveForm.Invoke(new MethodInvoker(delegate()
                {
                    session = null;
                    refreshAll();
                }));
        }

        private void buttonClose_Click(object sender, EventArgs e)
        {
            if (session != null)
                doBackGroundWork(disconnect, null, Messages.DISCONNECTING_FROM_MASTER);

            this.Close();
        }

        private void buttonRefresh_Click(object sender, EventArgs args)
        {
            refreshAll();
        }

        /// <summary>
        /// Using the current VM cache redraws the VM list.
        /// </summary>
        private void RedrawVMs()
        {
            dataGridViewVMs.SuspendLayout();
            try
            {
                dataGridViewVMs.Rows.Clear();
                foreach (VM vm in VMCache)
                {
                    DataGridViewRow r = new DataGridViewRow();
                    DataGridViewImageCell imCell = new DataGridViewImageCell();
                    imCell.Value = vm.other_config.ContainsKey(AUTO_RESTART_KEY) && vm.other_config[AUTO_RESTART_KEY] == AUTO_RESTART_VALUE 
                        ? Resources.Tick : Resources.cross;
                    r.Cells.Add(imCell);
                    DataGridViewTextBoxCell txCell = new DataGridViewTextBoxCell();
                    txCell.Value = vm.name_label;
                    r.Cells.Add(txCell);
                    r.Tag = vm;
                    dataGridViewVMs.Rows.Add(r);
                }
            }
            finally
            {
                dataGridViewVMs.ResumeLayout();
            }
        }

        /// <summary>
        /// Refreshes the VM cache used by the UI to populate the VM list,
        /// before calling a refresh with this new data and updating the
        /// enablement of the controls.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Not used</param>
        private void RefreshVMs(object sender, DoWorkEventArgs e)
        {
            List<VM> vms = new List<VM>();
            foreach (VM vm in VM.get_all_records(session).Values)
            {
                // The vm object is also used to represent templates and 
                // snapshots. We do not need to display these. Equally
                // We are not interested in the control domain VM.
                if (vm.is_a_template || vm.is_control_domain || vm.is_a_snapshot)
                    continue;

                vms.Add(vm);
            }
            vms.Sort(delegate(VM vm1, VM vm2) { return string.Compare(vm1.name_label, vm2.name_label); });
            Program.DInvoke(new MethodInvoker(delegate()
                {
                    VMCache = vms;
                    RedrawVMs();
                    updateEnabledState();
                }));
        }

        /// <summary>
        /// Returns true if the VM has the auto restart key set to true in its
        /// other config map.
        /// </summary>
        /// <param name="vm">The VM to query</param>
        /// <returns></returns>
        private bool IsAutoRestartVM(VM vm)
        {
            return vm.other_config.ContainsKey(AUTO_RESTART_KEY) && vm.other_config[AUTO_RESTART_KEY] == AUTO_RESTART_VALUE;
        }

        private void dataGridViewVM_SelectionChanged(object sender, EventArgs e)
        {
            updateEnabledState();
        }

        private void textBoxPassword_KeyUp(object sender, KeyEventArgs e)
        {
            // If the user is typing in the password box and presses enter
            // we read this as a request to connect.
            if (e.KeyCode == Keys.Enter)
            {
                doBackGroundWork(connectToServer, textBoxServerIP.Text, Messages.CONNECTING_TO_MASTER);
            }
        }

        /// <summary>
        /// Updates the VMCache and the UI, or if no session is connected
        /// resets the UI and clears the cache.
        /// </summary>
        private void refreshAll()
        {
            if (session == null)
            {
                VMCache.Clear();
                RedrawVMs();
                updateEnabledState();
            }
            else
                doBackGroundWork(RefreshVMs, null, Messages.REFRESHING_VMS);
        }

    }
}
